package org.example.system.service.impl.oss;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import io.minio.*;
import io.minio.errors.MinioException;
import io.minio.http.Method;
import io.minio.messages.Item;
import jakarta.annotation.PostConstruct;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.apache.commons.compress.utils.IOUtils;
import org.example.system.model.dto.FileInfo;
import org.example.system.model.dto.TreeFileList;
import org.example.system.model.dto.TreeListChildren;
import org.example.system.service.OssService;
import org.example.system.service.SysUserService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * MinIO 文件上传服务类
 *
 * @author LY
 */
@Component
@ConditionalOnProperty(value = "oss.type", havingValue = "minio")
@ConfigurationProperties(prefix = "oss.minio")
@RequiredArgsConstructor
@Data
public class MinioOssServiceImpl implements OssService {

    private final SysUserService sysUserService;

    /**
     * 服务Endpoint
     */
    private String endpoint;
    /**
     * 访问凭据
     */
    private String accessKey;
    /**
     * 凭据密钥
     */
    private String secretKey;
    /**
     * 存储桶名称
     */
    private String bucketName;
    /**
     * 自定义域名
     */
    private String customDomain;

    private MinioClient minioClient;

    // 依赖注入完成之后执行初始化
    @PostConstruct
    public void init() {
        minioClient = MinioClient.builder()
                .endpoint(endpoint)
                .credentials(accessKey, secretKey)
                .build();
    }

    /**
     * 文件上传
     *
     * @param file 表单文件对象
     * @return FileInfo
     */
    @Override
    public FileInfo uploadFile(MultipartFile file) {
        return upMinIo(file);
    }

    /**
     * 文件上传到 Minio 服务器
     *
     * @param file 文件对象
     * @return 文件上传信息
     */
    private FileInfo upMinIo(MultipartFile file) {
        // 生成文件名(日期文件夹)
        String originalFilename = file.getOriginalFilename();
        if (StrUtil.isBlank(originalFilename)) {
            throw new RuntimeException("文件名不能为空");
        }

        // 生成UUID 作为文件名
        String fileSuffix = originalFilename.substring(originalFilename.lastIndexOf(".")); // .png
        originalFilename = UUID.randomUUID().toString().replace("-","") + fileSuffix;

        // 文件名相同会进行覆盖
        String fileName = DateUtil.format(LocalDateTime.now(), "yyyyMMdd") + "/" + originalFilename;
        //  try-with-resource 语法糖自动释放流
        try (InputStream inputStream = file.getInputStream()) {
            // 文件上传
            PutObjectArgs putObjectArgs = PutObjectArgs.builder()
                    .bucket(bucketName)
                    .object(fileName)
                    .contentType(file.getContentType())
                    .stream(inputStream, inputStream.available(), -1)
                    .build();
            minioClient.putObject(putObjectArgs);

            // 返回文件路径
            String fileUrl;
            // 未配置自定义域名
            if (StrUtil.isBlank(customDomain)) {
                fileUrl = getFileUrl(fileName);
            } else { // 配置自定义文件路径域名
                fileUrl = customDomain + '/' + bucketName + "/" + fileName;
            }

            FileInfo fileInfo = new FileInfo();
            fileInfo.setName(fileName);
            fileInfo.setUrl(fileUrl);
            return fileInfo;
        } catch (Exception e) {
            throw new RuntimeException("文件上传失败");
        }
    }

    /**
     * 头像上传
     *
     * @param file 表单文件对象
     * @return FileInfo
     */
    @Override
    public FileInfo uploadAvatar(MultipartFile file) {
        FileInfo fileInfo = upMinIo(file);
        // 更新数据库中当前用户的头像存储地址
        boolean b = sysUserService.updateAvatarUrl(fileInfo.getUrl());

        if (b) {
            return fileInfo;
        } else {
            throw new RuntimeException("头像上传失败");
        }
    }

    /**
     * 文件下载
     *
     * @param fileName 文件名
     * @return 下载文件对象信息
     */
    @Override
    public ResponseEntity<InputStreamResource> downLoadFile(String fileName) {
        try {
            GetObjectArgs args = GetObjectArgs.builder()
                    .bucket(bucketName)
                    .object(fileName)
                    .build();
            InputStream inputStream = minioClient.getObject(args);
            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(inputStream.readAllBytes());

            // 设置响应头触发浏览器下载
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
            headers.setContentDispositionFormData("attachment", fileName);

            return ResponseEntity.ok().headers(headers).body(new InputStreamResource(byteArrayInputStream));
        } catch (Exception e) {
            throw new RuntimeException("文件下载失败");
        }
    }

    /**
     * 批量获取文件 url
     *
     * @return 文件 url 列表
     */
    @Override
    public List<TreeFileList> getFileList() throws NoSuchAlgorithmException, IOException, InvalidKeyException {
        Iterable<Result<Item>> results;
        List<FileInfo> fileUrlList = new ArrayList<>();
        ListObjectsArgs args = ListObjectsArgs.builder()
                .bucket(bucketName)
                .recursive(true)
                .build();
        try {
            // 列出桶中所有对象
            results = minioClient.listObjects(args);
            for (Result<Item> result : results) {
                // 取得桶下的所有文件名
                String fileName = result.get().objectName();
                String fileUrl = getFileUrl(fileName);
                // 设置文件名以及文件 url
                FileInfo fileInfo = new FileInfo();
                fileInfo.setName(fileName);
                fileInfo.setUrl(fileUrl);
                fileUrlList.add(fileInfo);
            }
        } catch (MinioException e) {
            throw new RuntimeException("获取文件列表失败");
        }

        return buildFileList(fileUrlList);
    }

    /**
     * 删除单个文件
     *
     * @param fileName 文件名
     */
    @Override
    public void deleteFile(String fileName) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
        try {
            minioClient.removeObject(RemoveObjectArgs.builder()
                    .bucket(bucketName)
                    .object(fileName)
                    .build());
        } catch (MinioException e) {
            throw new RuntimeException("删除文件失败");
        }
    }

    /**
     * 删除多个文件
     *
     * @param fileNameList 文件名列表
     */
    @Override
    public void deleteFileBatch(List<String> fileNameList) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
        for (String fileName : fileNameList) {
            deleteFile(fileName);
        }
    }

    /**
     * 批量文件上传
     *
     * @param files 文件列表
     * @return 上传后的文件信息集合
     */
    @Override
    public List<FileInfo> uploadFiles(List<MultipartFile> files) {
        List<FileInfo> fileInfoList = new ArrayList<>();
        for (MultipartFile file : files) {
            FileInfo fileInfo = upMinIo(file);
            fileInfoList.add(fileInfo);
        }

        return fileInfoList;
    }

    /**
     * 文件批量下载
     *
     * @param fileNameList 文件名列表
     * @return zip 文件
     */
    @Override
    public ResponseEntity<byte[]> downLoadFileZip(List<String> fileNameList) {
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
             ZipOutputStream zos = new ZipOutputStream(baos)) {
            // 遍历文件名列表，从 Minio获取文件并且添加到 zip 文件中
            for (String fileName : fileNameList) {
                GetObjectArgs args = GetObjectArgs.builder()
                        .bucket(bucketName)
                        .object(fileName)
                        .build();
                InputStream inputStream = minioClient.getObject(args);
                // 写入 zip 文件
                ZipEntry zipEntry = new ZipEntry(fileName);
                zos.putNextEntry(zipEntry);
                IOUtils.copy(inputStream, zos);
                zos.closeEntry();

                // 关闭输入流
                inputStream.close();
            }

            // 设置响应头
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
            headers.setContentDispositionFormData("attachment", "file.zip");

            // 返回 zip 文件
            return ResponseEntity.ok()
                    .headers(headers)
                    .body(baos.toByteArray());
        } catch (Exception e) {
            throw new RuntimeException("文件下载失败");
        }
    }

    /**
     * 查询 Minio 文件系统中文件的 url
     *
     * @param fileName 文件名
     * @return 文件 url
     */
    private String getFileUrl(String fileName) throws NoSuchAlgorithmException, IOException, InvalidKeyException, MinioException {
        String newFileUrl;
        try {
            GetPresignedObjectUrlArgs getPresignedObjectUrlArgs = GetPresignedObjectUrlArgs.builder()
                    .bucket(bucketName)
                    .object(fileName)
                    .method(Method.GET)
                    .build();
            newFileUrl = minioClient.getPresignedObjectUrl(getPresignedObjectUrlArgs);
            newFileUrl = newFileUrl.substring(0, newFileUrl.indexOf("?"));
        } catch (MinioException e) {
            throw new RuntimeException("获取文件 url 失败");
        }
        return newFileUrl;
    }

    /**
     * 将 Minio 文件列表转换为前端需要的树形格式。
     *
     * @param minioFileList Minio 文件列表
     * @return 格式化之后的数据
     */
    private List<TreeFileList> buildFileList(List<FileInfo> minioFileList) {
        List<TreeFileList> fileList = new ArrayList<>();
        Map<String, List<TreeListChildren>> folderMap = new HashMap<>();

        for (FileInfo file : minioFileList) {
            TreeFileList fileList1 = new TreeFileList();
            int isDirIndex = file.getName().indexOf("/");
            if (isDirIndex == -1) {
                // 文件情况下
                fileList1.setId(UUID.randomUUID().toString());
                fileList1.setName(file.getName());
                fileList1.setUrl(file.getUrl());
                fileList1.setDir(false);
                fileList1.setFullPath(file.getName());
                fileList.add(fileList1);
            } else {
                // 文件夹情况下
                String folderName = file.getName().substring(0, isDirIndex);
                if (!folderMap.containsKey(folderName)) {
                    folderMap.put(folderName, new ArrayList<>());
                    fileList1.setId(UUID.randomUUID().toString());
                    fileList1.setName(folderName);
                    fileList1.setUrl("");
                    fileList1.setDir(true);
                    fileList1.setChildren(new ArrayList<>());
                    fileList.add(fileList1);
                }

                // 添加到对应的文件夹children中
                String fileName = file.getName().substring(isDirIndex + 1);
                TreeListChildren childNode = new TreeListChildren();
                childNode.setName(fileName);
                childNode.setUrl(file.getUrl());
                childNode.setFullPath(file.getName());
                childNode.setParentPath(folderName);
                folderMap.get(folderName).add(childNode);
            }
        }

        // 递归处理文件夹的 children
        for (TreeFileList file : fileList) {
            if (file.isDir()) {
                file.setChildren(folderMap.get(file.getName()));
            }
        }
        return fileList;
    }
}
